Skip to content

fix: redact project credentials on egress - #1702

Merged
giswqs merged 16 commits into
mainfrom
fix/issue-1680-redact-project-credentials
Aug 4, 2026
Merged

fix: redact project credentials on egress#1702
giswqs merged 16 commits into
mainfrom
fix/issue-1680-redact-project-credentials

Conversation

@giswqs

@giswqs giswqs commented Aug 4, 2026

Copy link
Copy Markdown
Member

Summary

  • centralize project credential redaction in @geolibre/core
  • enforce redaction for Share, standalone HTML, collaboration, embed snapshots, and Python serialization
  • prompt before retaining credentials in local saves and notify users after sharing redacted projects
  • document the egress invariant and conservative external-plugin behavior
  • add redaction coverage for core, share, HTML export, and Python egress paths

Verification

  • npm run build
  • npm run test:frontend (5,051 passed, 1 skipped)
  • Python scripting tests (35 passed)
  • pre-commit on all changed files
  • browser verification in light and dark themes with authenticated 3D Tiles and geocoding credential sentinels

Fixes #1680

Summary by CodeRabbit

  • New Features
    • Project sharing, collaboration, embeds, and HTML exports now automatically remove credentials and sensitive connection details.
    • Share dialogs report how many credential fields were excluded.
    • Python exports redact credentials by default, with an option to preserve them for trusted local saves.
  • Bug Fixes
    • Improved protection for URLs, headers, API keys, environment variables, and plugin settings.
    • Added validation for invalid project content during sharing.
  • Documentation
    • Documented credential protection and safe export behavior.
  • Tests
    • Added coverage for credential removal across sharing, exports, and Python workflows.

Copilot AI lite review requested due to automatic review settings August 4, 2026 19:32
@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Projects now redact credentials before sharing, exporting, embedding, collaboration, and Python serialization. Local saves can retain credentials when explicitly requested. Sharing reports removed credential fields, with updated localized messages and regression coverage.

Changes

Credential-safe project egress

Layer / File(s) Summary
Central credential redaction
packages/core/src/credentials.ts, packages/core/src/index.ts, python/src/geolibre/project.py
Adds recursive redaction for credential fields, URLs, headers, metadata, environment variables, geocoder keys, and plugin settings. Preserves broker references and reports redacted paths.
Desktop egress integration
apps/geolibre-desktop/src/components/layout/*, apps/geolibre-desktop/src/hooks/*, apps/geolibre-desktop/src/lib/*, apps/geolibre-desktop/src/i18n/locales/*
Applies redaction to sharing, saves, HTML export, collaboration snapshots, and embed snapshots. The share dialog displays the removed credential count. Locales describe credential removal and recipient requirements.
Python egress behavior
python/src/geolibre/geolibre.py
Redacts credentials by default from project, file, and HTML exports. keep_credentials=True preserves credentials for project and file output.
Egress validation and documentation
tests/*, python/tests/test_scripting.py, docs/*
Adds tests for redaction, preserved broker references, source immutability, idempotence, sharing, HTML export, and Python output. Documents credential handling and plugin settings.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Desktop
  participant Core
  participant ShareService
  participant Collaboration
  participant PythonAPI
  Desktop->>Core: redactProjectCredentials(project)
  Core-->>Desktop: sanitized project and redaction count
  Desktop->>ShareService: upload serialized sanitized project
  Desktop->>Collaboration: send sanitized snapshot
  PythonAPI->>Core: redact project for export
  Core-->>PythonAPI: sanitized project
Loading

Possibly related PRs

Poem

A rabbit checks each secret key,
Then cleans the map for all to see.
Shared paths report what went away,
Local saves keep keys when they may.
Safe projects hop from shore to shore.

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR covers the main egress paths and tests, but the summary does not show plugin-declared sensitive fields or a schema-level guard test. Add plugin-declared sensitive-field support and a schema-level test that requires an explicit sensitivity decision for new credential-bearing fields.
Docstring Coverage ⚠️ Warning Docstring coverage is 54.17% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the primary change: redacting project credentials during egress.
Out of Scope Changes check ✅ Passed The code, tests, documentation, localization, and Python API changes all support credential redaction and project egress requirements.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/issue-1680-redact-project-credentials

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

🔍 Cloudflare PR preview

Item Value
Site https://79792062.geolibre-preview.pages.dev
Demo app https://79792062.geolibre-preview.pages.dev/demo/
Commit 9526ddc

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Not ready to approve

There are security-relevant edge cases where credential redaction can be bypassed (depth cap returning unredacted values) and Python URL param coverage is incomplete (missing subscription-key variants).

Once you've addressed the issues Copilot identified, you can request another Copilot review.

This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.

Pull request overview

This PR enforces a “no credentials on egress” invariant by introducing centralized credential redaction in @geolibre/core and applying it consistently across share uploads, standalone HTML export, collaboration/embed snapshots, local save prompting, and Python serialization paths, with accompanying docs and test coverage.

Changes:

  • Add @geolibre/core credential registry + redaction utilities (redactCredentials, redactProjectCredentials, redactUrlCredentials) and wire them into all external-project egress paths.
  • Update UI/egress flows to (a) prompt before keeping credentials in local saves and (b) notify after sharing when credentials were removed.
  • Add tests covering core redaction + share/html/Python egress paths; document the invariant and plugin-settings behavior.
File summaries
File Description
tests/share-geolibre.test.ts Adds coverage asserting share uploads redact credentials before network egress.
tests/share-fetch.test.ts Updates fixture project content to use core serialization helpers.
tests/project-credentials.test.ts Introduces unit tests for the central credential registry and redaction behavior (including idempotency).
tests/html-export.test.ts Adds coverage asserting standalone HTML export payload redacts credentials.
python/tests/test_scripting.py Adds coverage ensuring Python to_project(), to_html(), and save_project() redact by default.
python/src/geolibre/project.py Implements Python-side credential redaction for project dicts and URL-like strings.
python/src/geolibre/geolibre.py Applies redaction to HTML export + adds keep_credentials flags for project egress APIs.
packages/core/src/index.ts Re-exports new credential redaction APIs from core.
packages/core/src/credentials.ts Adds the core credential registry and redaction implementation used across egress paths.
docs/project-format.md Documents the credential-redaction invariant for projects leaving the local workspace.
docs/plugin-api.md Documents that plugins.settings is treated as sensitive and omitted on egress by default.
apps/geolibre-desktop/src/lib/share-geolibre.ts Forces share uploads to validate + redact content before sending requests.
apps/geolibre-desktop/src/lib/html-export.ts Redacts credentials before embedding project JSON in standalone HTML exports.
apps/geolibre-desktop/src/lib/build-project-snapshot.ts Adds a shared “egress snapshot” builder that redacts credentials for collaboration/embed.
apps/geolibre-desktop/src/hooks/useProjectFileActions.ts Prompts users before saving credential-bearing projects and relies on centralized redaction output.
apps/geolibre-desktop/src/hooks/useEmbedBridge.ts Switches embed snapshots to the redacted egress snapshot builder.
apps/geolibre-desktop/src/hooks/useCollaboration.ts Switches collaboration snapshots to the redacted egress snapshot builder.
apps/geolibre-desktop/src/components/layout/TopToolbar.tsx Redacts + counts removed credential fields when preparing share payloads.
apps/geolibre-desktop/src/components/layout/ShareProjectDialog.tsx Displays a post-share notice when credential fields were removed.
apps/geolibre-desktop/src/i18n/locales/en.json Adds/updates strings for credential stripping prompts and post-share notice.
apps/geolibre-desktop/src/i18n/locales/ar.json Adds/updates localized strings for credential stripping + share notice.
apps/geolibre-desktop/src/i18n/locales/de.json Adds/updates localized strings for credential stripping + share notice.
apps/geolibre-desktop/src/i18n/locales/es.json Adds/updates localized strings for credential stripping + share notice.
apps/geolibre-desktop/src/i18n/locales/fr.json Adds/updates localized strings for credential stripping + share notice.
apps/geolibre-desktop/src/i18n/locales/hi.json Adds/updates localized strings for credential stripping + share notice.
apps/geolibre-desktop/src/i18n/locales/id.json Adds/updates localized strings for credential stripping + share notice.
apps/geolibre-desktop/src/i18n/locales/it.json Adds/updates localized strings for credential stripping + share notice.
apps/geolibre-desktop/src/i18n/locales/ja.json Adds/updates localized strings for credential stripping + share notice.
apps/geolibre-desktop/src/i18n/locales/ka.json Adds/updates localized strings for credential stripping + share notice.
apps/geolibre-desktop/src/i18n/locales/ko.json Adds/updates localized strings for credential stripping + share notice.
apps/geolibre-desktop/src/i18n/locales/nl.json Adds/updates localized strings for credential stripping + share notice.
apps/geolibre-desktop/src/i18n/locales/pt.json Adds/updates localized strings for credential stripping + share notice.
apps/geolibre-desktop/src/i18n/locales/ru.json Adds/updates localized strings for credential stripping + share notice.
apps/geolibre-desktop/src/i18n/locales/th.json Adds/updates localized strings for credential stripping + share notice.
apps/geolibre-desktop/src/i18n/locales/tr.json Adds/updates localized strings for credential stripping + share notice.
apps/geolibre-desktop/src/i18n/locales/zh.json Adds/updates localized strings for credential stripping + share notice.
Review details
  • Files reviewed: 36/36 changed files
  • Comments generated: 2
  • Review effort level: Lite

We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.

Comment thread python/src/geolibre/project.py Outdated
Comment thread packages/core/src/credentials.ts Outdated
- Fail closed when project configuration exceeds the redaction depth cap.
- Redact subscription-key URL parameters from Python project egress.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/geolibre-desktop/src/components/layout/TopToolbar.tsx`:
- Around line 2015-2019: The project export result in the surrounding
ShareProject flow currently reports redacted.redactedPaths.length, which counts
paths rather than removed credential fields. Update the core redaction helper to
return the total number of removed fields and use that value for redactedCount
in the returned object; otherwise change the localized notice to explicitly
describe redaction paths or groups.

In `@apps/geolibre-desktop/src/i18n/locales/tr.json`:
- Line 1176: Update the Turkish credentialsRemoved translation to use “aracılı
bir referans” instead of “aracılı bir başvuru,” matching the
brokered-credential-reference meaning defined by en.json.

In `@packages/core/src/credentials.ts`:
- Around line 202-205: Update redactProjectCredentials to sanitize
project.basemapStyleUrl with redactUrlCredentials before constructing the
returned project; include the redacted URL only when it differs from the
original, while preserving the existing spread and other project fields.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: d647377f-e9a6-4e3d-967a-e2b38cb1c740

📥 Commits

Reviewing files that changed from the base of the PR and between 6d615a9 and 988ab26.

📒 Files selected for processing (36)
  • apps/geolibre-desktop/src/components/layout/ShareProjectDialog.tsx
  • apps/geolibre-desktop/src/components/layout/TopToolbar.tsx
  • apps/geolibre-desktop/src/hooks/useCollaboration.ts
  • apps/geolibre-desktop/src/hooks/useEmbedBridge.ts
  • apps/geolibre-desktop/src/hooks/useProjectFileActions.ts
  • apps/geolibre-desktop/src/i18n/locales/ar.json
  • apps/geolibre-desktop/src/i18n/locales/de.json
  • apps/geolibre-desktop/src/i18n/locales/en.json
  • apps/geolibre-desktop/src/i18n/locales/es.json
  • apps/geolibre-desktop/src/i18n/locales/fr.json
  • apps/geolibre-desktop/src/i18n/locales/hi.json
  • apps/geolibre-desktop/src/i18n/locales/id.json
  • apps/geolibre-desktop/src/i18n/locales/it.json
  • apps/geolibre-desktop/src/i18n/locales/ja.json
  • apps/geolibre-desktop/src/i18n/locales/ka.json
  • apps/geolibre-desktop/src/i18n/locales/ko.json
  • apps/geolibre-desktop/src/i18n/locales/nl.json
  • apps/geolibre-desktop/src/i18n/locales/pt.json
  • apps/geolibre-desktop/src/i18n/locales/ru.json
  • apps/geolibre-desktop/src/i18n/locales/th.json
  • apps/geolibre-desktop/src/i18n/locales/tr.json
  • apps/geolibre-desktop/src/i18n/locales/zh.json
  • apps/geolibre-desktop/src/lib/build-project-snapshot.ts
  • apps/geolibre-desktop/src/lib/html-export.ts
  • apps/geolibre-desktop/src/lib/share-geolibre.ts
  • docs/plugin-api.md
  • docs/project-format.md
  • packages/core/src/credentials.ts
  • packages/core/src/index.ts
  • python/src/geolibre/geolibre.py
  • python/src/geolibre/project.py
  • python/tests/test_scripting.py
  • tests/html-export.test.ts
  • tests/project-credentials.test.ts
  • tests/share-fetch.test.ts
  • tests/share-geolibre.test.ts

Comment thread apps/geolibre-desktop/src/components/layout/TopToolbar.tsx
Comment thread apps/geolibre-desktop/src/i18n/locales/tr.json Outdated
Comment thread packages/core/src/credentials.ts
- redact credential parameters from basemap style URLs

- report individual redacted fields and correct Turkish wording
Comment thread packages/core/src/credentials.ts
Comment thread packages/core/src/credentials.ts
Comment thread python/src/geolibre/project.py Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
python/tests/test_scripting.py (1)

316-351: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Extend this test to prove encoded redaction and opt-in file retention.

The fixture covers only plain subscription-key. Add an encoded form such as subscription%2Dkey=... and assert that its secret is absent from serialized, html, and saved.

The final assertion checks only that plugin settings are non-empty. Assert a known credential value for keep_credentials=True. Also call save_project(..., keep_credentials=True) and assert that the retained secret is present in that file.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@python/tests/test_scripting.py` around lines 316 - 351, Extend the test
fixture in the project serialization flow to include an encoded query key such
as subscription%2Dkey with a known secret, then add that secret to the redaction
assertions for serialized, HTML, and saved output. Update the
keep_credentials=True assertion to verify the known plugin credential value, and
save with keep_credentials=True to a separate file and assert that the retained
secret appears there.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/core/src/credentials.ts`:
- Around line 124-130: In redactConfigurationValue, evaluate the depth >=
MAX_REDACT_DEPTH fail-closed check before the scalar string branch and its
redactUrlCredentials return. Preserve the existing redactedPaths update and
undefined return so strings at the traversal boundary are omitted like other
deeply nested values.

In `@python/src/geolibre/project.py`:
- Around line 45-46: Update _redact_url to URL-decode each query parameter name
before checking it against _CREDENTIAL_URL_PARAMS, while preserving the original
query pair when retaining parameters and continuing to redact matching
credentials.

In `@tests/project-credentials.test.ts`:
- Around line 82-91: Strengthen the test in the “fails closed when configuration
exceeds the traversal depth” case by placing a marker scalar exactly at or
beyond MAX_REDACT_DEPTH, rather than only checking a deeply nested serialized
object. Assert that this scalar marker is removed from the serialized result and
that its specific redacted path is included in result.redactedPaths.

---

Outside diff comments:
In `@python/tests/test_scripting.py`:
- Around line 316-351: Extend the test fixture in the project serialization flow
to include an encoded query key such as subscription%2Dkey with a known secret,
then add that secret to the redaction assertions for serialized, HTML, and saved
output. Update the keep_credentials=True assertion to verify the known plugin
credential value, and save with keep_credentials=True to a separate file and
assert that the retained secret appears there.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: f920822e-16ee-4746-a96e-efcceabe2261

📥 Commits

Reviewing files that changed from the base of the PR and between 988ab26 and 2e476bc.

📒 Files selected for processing (4)
  • packages/core/src/credentials.ts
  • python/src/geolibre/project.py
  • python/tests/test_scripting.py
  • tests/project-credentials.test.ts

Comment thread packages/core/src/credentials.ts
Comment thread python/src/geolibre/project.py Outdated
Comment thread tests/project-credentials.test.ts
@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

🔍 GitHub Pages PR preview

Item Value
Site Deploy failed. See the job log.
Demo app Unavailable
Commit 9526ddc

Comment thread apps/geolibre-desktop/src/hooks/useCollaboration.ts
Comment thread apps/geolibre-desktop/src/hooks/useProjectFileActions.ts Outdated
Comment thread packages/core/src/credentials.ts Outdated
Comment thread packages/core/src/credentials.ts
@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Code review

Bugs

  • apps/geolibre-desktop/src/hooks/useCollaboration.ts:79 (inline comment posted) — sendSnapshot now builds/stores lastContentRef.current from the redacted buildProjectEgressSnapshot, but applyRemoteSnapshot (line 108, untouched by this PR) still baselines it from the un-redacted buildProjectSnapshot. Whenever the project holds any credentials, the dedup check content === lastContentRef.current will mismatch right after any remote snapshot is applied, causing one spurious extra snapshot broadcast. Confidence: medium-high.
  • apps/geolibre-desktop/src/hooks/useProjectFileActions.ts:845 (inline comment posted) — passes redacted.redactedPaths.length (deduplicated paths) instead of redacted.redactedCount (actual field count) into the "Strip credentials?" prompt, understating the count shown to the user whenever multiple secrets share one path (e.g. several environment variables). ShareProjectDialog/TopToolbar.tsx correctly use redactedCount for the equivalent message. Confidence: medium.

Security

  • packages/core/src/credentials.ts:35-56 (inline comment posted) — URL_CREDENTIAL_PARAMS is a fixed denylist; it covers subscription-key/subscriptionkey but not the equally plausible api-key (only api_key/apikey), and misses other common names (auth, secret, sasToken, bearer, …). The mirrored Python list has the same gap. docs/project-format.md states the invariant in absolute terms ("removes credential-bearing fields and authentication parameters in URLs"), which could give false confidence for an unlisted provider param. Confidence: medium.

Quality

  • packages/core/src/credentials.ts:3-22 (inline comment posted) — PROJECT_CREDENTIAL_FIELDS's docstring frames it as "the schema-level decision point," but only layerConfiguration actually drives redaction (SENSITIVE_KEYS); preferences/pluginState entries are enforced by separate hardcoded logic further down, so editing the registry alone wouldn't change behavior. Confidence: low-medium.
  • apps/geolibre-desktop/src/hooks/useEmbedBridge.ts:39 — the "Trust model" doc comment ("the embedding host is... receives the entire project state") is now stale since postState/applyLoad send the redacted buildProjectEgressSnapshot, not the entire project. Not part of the diff hunk so no inline comment was posted. Confidence: low.
  • python/src/geolibre/project.py:86 (_redact_config) — unlike the JS redactConfigurationValue, which fails closed at MAX_REDACT_DEPTH = 12, the Python version has no recursion guard, so a pathologically deep/nested layer.source/metadata dict could raise RecursionError in to_html()/save_project() instead of degrading gracefully. Low real-world likelihood; not part of the diff hunk so no inline comment was posted. Confidence: low.

CLAUDE.md

  • This PR introduces a new pair of mirrored magic constants (JS credentials.ts's field/param lists vs. Python project.py's _CREDENTIAL_FIELD_NAMES/_CREDENTIAL_URL_PARAMS) that must be kept in sync by hand, matching the exact pattern CLAUDE.md's Conventions section calls out repeatedly for other mirrored constants (MAX_VECTOR_PMTILES_ZOOM, MAX_VECTOR_BYTES, MAP_PANEL_SELECTOR, etc.), each with an explicit "whenever X is bumped, re-check Y" note and a regression test. This new pair has no such note and no cross-language drift test. Consider adding a short convention entry so a future edit to one list isn't silently missed in the other. Confidence: low-medium (a reasonable suggestion consistent with existing project convention, not a stated rule this PR violates).

Performance — no notable issues found; the redaction pass runs once per egress event on already-small project objects.

giswqs and others added 2 commits August 4, 2026 16:03
- sanitize basemap and plugin manifest URLs across TypeScript and Python

- harden URL parameter and userinfo redaction edge cases

- align collaboration snapshots and credential counts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/core/src/credentials.ts`:
- Around line 67-73: Update isCredentialParam to decode the normalized name
before applying lowercase normalization. Ensure percent-encoded alphabetic
characters are lowercased after decoding, so URL_CREDENTIAL_PARAMS and the
x-amz- prefix checks handle encoded credential names correctly while preserving
malformed-input handling.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 435d91b7-6bbd-49f1-941e-4c23194e3118

📥 Commits

Reviewing files that changed from the base of the PR and between 641e392 and 5c1a0b4.

📒 Files selected for processing (6)
  • apps/geolibre-desktop/src/hooks/useCollaboration.ts
  • apps/geolibre-desktop/src/hooks/useProjectFileActions.ts
  • packages/core/src/credentials.ts
  • python/src/geolibre/project.py
  • python/tests/test_scripting.py
  • tests/project-credentials.test.ts

Comment thread packages/core/src/credentials.ts Outdated
Comment thread packages/core/src/credentials.ts Outdated
Comment thread python/src/geolibre/project.py
Comment thread python/src/geolibre/project.py Outdated
Comment thread packages/core/src/credentials.ts Outdated
@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Code review

Bugs

  • packages/core/src/credentials.ts (redactProjectCredentials, ~L209-217) and the parallel python/src/geolibre/project.py (redact_credentials, ~L122-127): only preferences.geocoding.apiKeys is cleared — forwardEndpoint/reverseEndpoint, the free-text custom geocoding endpoint URLs a user can set in Settings, are passed through untouched. Several built-in providers append the API key directly onto whichever endpoint is configured (packages/core/src/geocoding.ts), so a self-hosted/proxied endpoint with a credential embedded in its URL (e.g. ?key=SECRET) survives Share, HTML export, collaboration/embed snapshots, and Python egress — the exact class of leak this PR is meant to close. Confidence: high.

Quality

  • python/src/geolibre/project.py _redact_config (~L100-114) has no depth guard, unlike the JS redactConfigurationValue, which fails closed at MAX_REDACT_DEPTH = 12 (and is covered by a dedicated test). The two implementations are meant to enforce one shared invariant but now diverge in worst-case behavior; deeply nested config could also raise RecursionError in Python instead of degrading safely. Confidence: medium.
  • packages/core/src/credentials.ts L156: the GeoJSON early-return (isGeoJsonPayload) returns the original object by reference instead of cloning, unlike the Python port which does copy.deepcopy. Harmless today since every caller serializes the result immediately, but it undercuts the "detached project" guarantee documented on the function. Confidence: low.
  • apps/geolibre-desktop/src/hooks/useProjectFileActions.ts (~L947-951, outside the diff hunk so not inline-commentable): the comment above buildEmbeddedProject/buildProjectHtml in handleExportHtml still says stripping happens "here," but redaction now happens inside buildProjectHtml via redactCredentials and covers all credential fields, not just env vars. Stale comment, no functional impact. Confidence: low.

No issues found in the i18n translation additions, the double-redaction in TopToolbar.tsx/share-geolibre.ts (redundant but idempotent and intentional defense-in-depth), or the new test coverage, which is thorough and correctly validates the depth-cutoff and broker-reference-preservation behavior that does exist.

Comment thread apps/geolibre-desktop/src/hooks/useProjectFileActions.ts Outdated
Comment thread packages/core/src/layer-library.ts Outdated
Comment thread packages/core/src/credentials.ts
@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

All inline comments are posted. Here is the final review summary.

Code review

Bugs

  • None found. The core redaction logic (redactUrlCredentials/redactConfigurationValue in packages/core/src/credentials.ts and their Python mirrors in python/src/geolibre/project.py) traced correctly against its own test suite, including the userinfo-stripping, query/fragment credential-parameter filtering, GeoJSON short-circuiting, and depth-capped fail-closed behavior. Confidence: high.

Security

  • python/src/geolibre/project.py's redact_credentials doesn't redact a layer's sourcePath, unlike the TS redactProjectCredentials, which does. Low practical impact since sourcePath is documented as a local filesystem path, but it's a parity gap between the two egress implementations. Confidence: medium.
  • Layer Library Export (packages/core/src/layer-library.ts, EXPORT_REDACTED_SOURCE_KEYS) — an egress path the code's own comment frames as "a far more direct exposure path than handing someone a whole project file" — still only strips the requestHeaders key, not the broader apiKey/token/password/clientSecret/connectionString set this PR introduces for the main project egress boundary. Pre-existing code, only its import was touched here, so may be intentionally out of scope. Confidence: medium.
  • redactProjectCredentials/redact_credentials don't cover project.models (Processing pipelines) or project.processingHistory, whose parameters bags could theoretically carry a credential for some future tool. This is explicitly acknowledged in a code comment, so it reads as a deliberate scope boundary rather than an oversight — flagged mainly because the new docs describe the invariant in fairly absolute terms. Confidence: low.

Performance

  • No issues found. The redaction pass short-circuits on GeoJSON payloads via structuredClone rather than recursively scanning feature properties, keeping the added per-snapshot cost (now run on every collaboration sync and embed post) roughly linear in configuration size rather than data size. Confidence: high.

Quality

  • runSaveProject (useProjectFileActions.ts) now shows the "Strip credentials?" prompt on every local save whenever any credential-bearing field exists (previously scoped to populated env vars only), with no "remember my choice." This broadens a per-save interruption to any project with a routinely authenticated layer. Likely intentional per the PR's stated goal, but worth confirming the frequency is acceptable. Confidence: medium.
  • askStripEnvVars/EnvStripPrompt naming wasn't updated even though its scope broadened from env vars to all credential types — a minor readability nit. Confidence: low.
  • A comment in useProjectFileActions.ts's HTML-export flow ("...and strip env vars") is now stale: the explicit env-var-stripping code it describes was removed in this PR and redaction now happens inside buildProjectHtml. Confidence: medium.

CLAUDE.md

  • No violations found. uv.lock/build-catalog/mirror-constant conventions aren't implicated by this change, and new user-facing strings correctly go through t() with catalogs updated across all locales.

- reuse the central structural-key registry for Layer Library exports

- redact Python layer source paths and document processing scope
Comment thread apps/geolibre-desktop/src/hooks/useEmbedBridge.ts Outdated
Comment thread packages/core/src/credentials.ts
@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Code review

Bugs

  • apps/geolibre-desktop/src/hooks/useEmbedBridge.ts:66 — The embed bridge now redacts every geolibre:state snapshot before posting it, but this same bridge is the sync channel for the Jupyter/anywidget Python widget, which its own surrounding docs (this file, embedHost.ts, _frontend.js, geolibre.py's Map docstring) describe as fully trusted, full-fidelity, two-way sync. Any store change (even a pan) now silently strips credentials from Python's self.project trait, which defeats the new keep_credentials=True option this PR adds to to_project()/save_project(). Medium-high confidence (traced through code, not runtime-verified).

Quality

  • Same location — the "fully trusted"/"broadcasts full project state" docstrings in useEmbedBridge.ts and embedHost.ts are now stale relative to the new redaction behavior, regardless of whether the change itself is intentional.
  • packages/core/src/credentials.ts:143-150 — the URL-credential sweep now runs over every string in layer.metadata/project.metadata (new territory vs. the pre-PR scope), not just URL-shaped fields, so free-form text that happens to contain a ?token=...-like substring could be silently mangled. Low-medium confidence, narrow edge case.
  • packages/core/src/layer-library.ts:710 — the Layer Library export sweep and the new central credentials.ts sweep now share a field registry but diverge on depth-limit failure mode (fail-open at depth 6 here vs. fail-closed at depth 12 in credentials.ts), which is inconsistent with the PR's stated single-invariant goal, though this specific behavior predates the PR. Low-medium confidence.

Security / Performance / CLAUDE.md

  • No additional findings. The core redaction logic (URL userinfo/query/fragment stripping, recursive field scrubbing, depth-limited fail-closed traversal, plugin-settings wholesale removal, Python/JS parity) is well-tested and consistent across Share, HTML export, collaboration, and Python egress paths; i18n locale updates look complete and consistent across all listed languages; no CLAUDE.md convention violations (translations use t(), no node_modules edits, no lockfile-affecting dependency changes) were observed.

- distinguish the co-located anywidget host from external embed egress

- retain redaction for standalone HTML and iframe hosts
Comment thread packages/core/src/credentials.ts
Comment thread apps/geolibre-desktop/src/hooks/useEmbedBridge.ts
Comment thread packages/core/src/layer-library.ts Outdated
Comment thread apps/geolibre-desktop/src/hooks/useProjectFileActions.ts Outdated
@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

All inline comments are posted. Here is the final summary.

Code review

Bugs

  • None found. The redaction pass is non-mutating (verified by the "detached inline GeoJSON" test and by tracing that redactConfigurationValue/redactProjectCredentials always build new objects/arrays), and the redactedCount accounting checks out against the test fixture (9 distinct redaction events, no double-counting despite the [...new Set(redactedPaths)] dedup on paths only).

Security

  • packages/core/src/credentials.ts: the object-key credential registry (PROJECT_CREDENTIAL_FIELDS.layerConfiguration / SENSITIVE_KEYS) is narrower than the URL-parameter registry (URL_CREDENTIAL_PARAMS) — names like secret, bearer, sasToken, authKey, subscriptionKey, pwd, sig are treated as credentials in a URL query string but not when they appear as a plain JSON field on a layer's source. Medium confidence.
  • apps/geolibre-desktop/src/hooks/useEmbedBridge.ts: the new trustedWidget flag that decides whether the embed bridge returns the full or redacted project snapshot is read verbatim off the inbound message from window.parent — i.e. self-declared by the very host it's meant to gate. For the ?embed=1 path, which the file's own docstring already documents as treating "whatever the framing parent is" as fully trusted, any such host can simply set trustedWidget: true to get the unredacted snapshot back, so the flag protects only well-behaved hosts, not adversarial ones. Medium confidence — this may be intentional (the widget genuinely needs credentials round-tripped), but it isn't documented as a caveat.
  • packages/core/src/layer-library.ts (redactSourceValue): fails open past its MAX_REDACT_DEPTH (returns the value unchanged), the opposite of the fail-closed design the PR's own new credentials.ts module uses at its depth cap, with an explicit comment explaining why fail-closed is correct. The PR touches this exact function but doesn't reconcile the inconsistency. Medium confidence.

Performance

  • No concerns; the redaction traversal is bounded by depth caps and runs only on egress paths (share/export/collab/embed), not on the hot render path.

Quality

  • useEmbedBridge.ts's docstring ("the embedding host is fully trusted and receives the entire project state") wasn't updated to reflect the new conditional (trustedWidget-gated) default-redact behavior — see inline comment. Medium-high confidence.
  • useProjectFileActions.ts: askStripEnvVars/envStripPrompt naming is stale now that the prompt covers all credential types, not just environment variables (the i18n strings were already generalized). Low severity, cosmetic.
  • docs/python.md isn't updated to mention the new keep_credentials parameter on Map.to_project()/Map.save_project() or the new default redaction behavior of to_html() — a user-facing API behavior change with no corresponding doc update. Low-medium confidence this matters given docs/project-format.md does cover the JS-side invariant.

CLAUDE.md

  • No violations found. i18n strings were added consistently across all locale files with en.json as source of truth; no MapLibre control CSS, WASM catalog, or other documented mirror invariants were touched.

giswqs added 3 commits August 4, 2026 17:04
- Extend PROJECT_CREDENTIAL_FIELDS.layerConfiguration with the credential
  names the URL registry already stripped but the object-key denylist
  missed (secret, bearer, auth, authKey, sasToken, subscriptionKey,
  signature, pwd), so a layer source storing one as a plain JSON field is
  no longer shipped in a "redacted" export.
- Derive URL_CREDENTIAL_PARAMS from that same list plus the names that are
  credentials only in a query string (`key` and the Azure SAS positional
  parameters sv/sr/st/se/sp/sig/skoid), and document why they stay
  URL-only: as configuration field names they collide with ordinary state
  such as an ArcGIS `sr`.
- Match names through a shared normalizer so apiKey/api_key/api-key/APIKEY
  are one entry on both sides, and route the Layer Library export sweep
  through the same isCredentialFieldName predicate instead of its own
  lowercase set, so the two paths cannot drift.
- Cover the new spellings, and the deliberately-kept `sr`/`key` fields,
  in tests/project-credentials.test.ts.
- packages/core/src/layer-library.ts: `redactSourceValue` now fails closed
  past its depth cap instead of returning the value unswept, matching the
  fail-closed stance `redactConfigurationValue` takes in credentials.ts.
  Only the exported bundle loses the value; the local IndexedDB entry is
  untouched.
- useEmbedBridge.ts / embedHost.ts: rewrite the trust-model docs, which
  still claimed the host receives the entire project state. Snapshots are
  redacted by default, and the `trustedWidget` opt-in is documented as
  self-declared by the host — a fidelity switch for the co-located widget,
  not a security boundary; the boundary is still the framing context.
- useProjectFileActions.ts / ProjectFileDialogs.tsx: rename
  EnvStripPrompt/envStripPrompt/askStripEnvVars to the credential-neutral
  CredentialStripPrompt/credentialStripPrompt/askStripCredentials, since
  the prompt now covers every credential type, not just env vars.
- docs/python.md: document that to_project()/save_project()/to_html()
  redact by default and that keep_credentials=True opts back out.
- Add a regression test for the fail-closed depth cap in redactSourceValue:
  a credential nested past MAX_REDACT_DEPTH must not reach the exported
  bundle. Fails against the previous fail-open behavior.
Comment thread python/src/geolibre/project.py
Comment thread apps/geolibre-desktop/src/hooks/useProjectFileActions.ts
@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Both inline comments posted successfully. Now finalizing the summary.

Code review

Bugs

  • None found beyond the redaction gap noted under Security — the store/UI wiring (TopToolbar, ShareProjectDialog, useEmbedBridge, useCollaboration) is internally consistent and covered by matching tests. Confidence: medium (reviewed the full data flow but did not execute the test suite).

Security

  • python/src/geolibre/project.py's _CREDENTIAL_FIELD_NAMES (object-key redaction for layer.source/metadata/sourcePath) is missing secret, bearer, auth, authKey, sasToken, subscriptionKey, signature, pwd — all present in the TS PROJECT_CREDENTIAL_FIELDS.layerConfiguration registry and explicitly covered by a JS test ("removes credential-named configuration fields in every spelling"). These names are only caught by Python's URL-parameter set, not its object-key set, so to_project()/save_project()/to_html() can ship a credential shaped as e.g. {"bearer": "..."} or {"pwd": "..."} in a layer source that the JS-side redaction would strip. Posted inline with a suggested fix. Confidence: high.
  • The trustedWidget self-declared flag in useEmbedBridge.ts is a known, clearly documented trust narrowing rather than a boundary — the PR's own comments are candid that any framing page can set it. Not a new gap, just worth confirming reviewers are comfortable with that residual risk. Confidence: low (already acknowledged by the authors).

Performance

  • redactProjectCredentials is invoked on every debounced store change for both collaboration (useCollaboration.ts) and embed (useEmbedBridge.ts) snapshots (every 250ms during active edits, including pure camera pans that don't touch layers), and does a structuredClone of any embedded GeoJSON payload plus a full recursive walk of layer source/metadata each time. Since the existing code already serializes the full project on every such change, this is incremental rather than new cost, but for projects with large embedded vector layers it adds a repeated full deep-clone that wasn't there before. Confidence: low-medium.
  • The Share flow redacts twice (once in TopToolbar.tsx's getProject, again inside uploadProjectToShare) — harmless correctness-wise (redaction is idempotent, tested) but doubles the traversal/clone cost for large projects. Confidence: low (intentional defense-in-depth, minor cost).

Quality

  • Stale comment in useProjectFileActions.ts (around the HTML-export handler) still describes stripping "env vars" locally, but that responsibility moved into buildProjectHtml/redactCredentials. Posted inline. Confidence: low.
  • Widening the local Save/Save-As "strip credentials?" prompt trigger from "any environment variables set" to "any redactable field present" means the prompt will now fire on ordinary local saves whenever a layer URL contains a key=-style parameter (common for many tile providers' non-secret app IDs), not just when real secrets are configured — a plausible UX regression versus the previous, narrower trigger. This appears to be an intentional, documented trade-off (docs/project-format.md), so flagging for awareness rather than as a defect. Confidence: low-medium.

CLAUDE.md

  • No violations found: i18n strings use t() and are added to en.json plus all locale catalogs; no direct MapLibre mutation; no new external tile/plugin hosts introduced. Confidence: medium.

- python/src/geolibre/project.py: `_CREDENTIAL_FIELD_NAMES` was missing the
  names added to the TS registry (secret, bearer, auth, authKey, sasToken,
  subscriptionKey, signature, pwd), so `to_project()`/`save_project()`/
  `to_html()` could ship a credential the JS egress path strips. Derive
  `_CREDENTIAL_URL_PARAMS` from that same set plus the query-string-only
  names, and match through a `_normalize_credential_name` mirror of the JS
  normalizer, with a parity test in python/tests/test_scripting.py.
- useProjectFileActions.ts: reword the stale HTML-export comment — this call
  site no longer strips environment variables itself; buildProjectHtml runs
  the central redaction pass.
}

const layers = (project.layers ?? []).map((layer, index) => ({
...layer,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, well-tested redaction design overall. One gap: the per-layer sweep only redacts source, metadata, and sourcePath (via ...layer then overriding those three) — layer.connection (in particular connection.lastError) passes through untouched.

Today's built-in refresh-error paths (layer-refresh.ts) only ever set lastError to generic strings like "Request failed with status 403", so nothing currently leaks. But connection.lastError is free-form text set from a caught error's .message, and it isn't in PROJECT_CREDENTIAL_FIELDS or swept by redactConfigurationValue. If a future refresh path (or a plugin driving connection) ever surfaces an error message that embeds the request URL (a common pattern, e.g. `Failed to fetch ${url}`), a token/API-key query parameter would ship unredacted through Share/HTML export/embed/collaboration. Worth adding connection to the sweep (or at least connection.lastError through redactUrlCredentials) for defense-in-depth, given the module's own stated goal is a "no-secret guarantee" for external egress. Low confidence this is exploitable today, but it's a blind spot in an otherwise very deliberately enumerated registry.

The Python mirror (python/src/geolibre/project.py, redact_credentials) has the identical gap since it also only touches source/metadata/sourcePath per layer.

@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Code review

Bugs

  • None found. The redaction pass in packages/core/src/credentials.ts (and its Python mirror in python/src/geolibre/project.py) is carefully constructed — depth-capped fail-closed recursion, GeoJSON payloads correctly excluded from the sweep to avoid corrupting feature data, URL/userinfo/query/fragment stripping handled consistently, and idempotency verified by tests. I traced the full egress surface (Share, HTML export, embed bridge, collaboration snapshots, Python to_project/save_project/to_html) and each path is wired through the central redaction helpers with no missed call sites.

Security

  • (Medium-low confidence) layer.connection (specifically connection.lastError, a free-form error-message string) is not covered by the credential redaction sweep in either the JS or Python implementation — only source, metadata, and sourcePath are swept per layer. No currently-shipped code path leaks a credential through it (built-in refresh errors are generic strings), but it's a blind spot relative to the module's stated "no-secret guarantee" if a future error path or plugin ever embeds a URL with a token in that field. Posted inline on packages/core/src/credentials.ts.
  • (Already mitigated, informational) useEmbedBridge.ts's trustedWidget flag is self-declared by the framing host and is explicitly documented as a fidelity switch rather than a security boundary — this is a deliberate, well-reasoned tradeoff by the author, not a defect.

Performance

  • No concerns. Redaction runs synchronously over already-in-memory project objects at export time; depth caps (12 in credentials.ts, 6 in layer-library.ts) bound worst-case recursion.

Quality

  • Minor: packages/core/src/credentials.ts and packages/core/src/layer-library.ts each define their own MAX_REDACT_DEPTH constant (12 vs. 6) rather than sharing one, and the comment in layer-library.ts says its fail-closed behavior is "matching redactConfigurationValue in credentials.ts," which could be misread as matching the depth value too. Cosmetic only.
  • The JS and Python credential registries (PROJECT_CREDENTIAL_FIELDS.layerConfiguration / _CREDENTIAL_FIELD_NAMES) are duplicated by hand with a comment asking maintainers to keep them in sync manually — a drift risk, but reasonable given the two runtimes can't share a module, and it's explicitly called out.

CLAUDE.md

  • No violations found. New i18n strings use t() and are present in all locale catalogs with consistent {{count}} placeholders; no MapLibre control CSS, no Tauri CSP, no Whitebox/menu-catalog, or other guarded-invariant files were touched.

@giswqs
giswqs merged commit 43ae1b4 into main Aug 4, 2026
36 checks passed
@giswqs
giswqs deleted the fix/issue-1680-redact-project-credentials branch August 4, 2026 21:35
giswqs added a commit that referenced this pull request Aug 4, 2026
The egress redaction pass added in #1702 sweeps each layer's `source`,
`metadata`, and `sourcePath`, but leaves `connection` untouched.
`connection.lastError` is free-form text taken from a caught error, and a
refresh path that words it as `Failed to fetch ${url}` would carry the
request's credential parameters straight through Share, HTML export,
embed, and collaboration.

Today's built-in refresh paths only ever store generic status strings, so
nothing leaks in practice — this closes the blind spot rather than a live
bug, so the "no-secret guarantee" does not depend on how a future error
message is phrased.

- packages/core/src/credentials.ts: sweep `layer.connection` through
  `redactConfigurationValue` alongside the other three fields.
- python/src/geolibre/project.py: the same field in the Python mirror.
- Tests on both sides assert the token is stripped while the rest of the
  connection record (interval, timestamps) survives.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Guarantee published and exported projects never carry credentials

2 participants